# -*- coding: UTF-8 -*-
"""
This is a HTTP/REST client library, primarily designed for use as a
`Robot Framework <http://robotframework.org/>`_ test library. It provides
keywords for calling REST-style services and inspecting the response.

Xuan Xu, Accedo Broadband AB.
"""

__author__ = "Niklas Lindström, Xuan Xu"
__version__ = "0.1a"


import re
from urlparse import urljoin
import httplib
from warnings import warn
import hashlib
from functools import wraps

import mimetypes
import json
import jsonpointer


def load_json(json_string):
    try:
        return json.loads(json_string)
    except ValueError, e:
        raise ValueError("Could not parse '%s' as JSON: %s" % (json_string, e))

def _with_json(f):
    @wraps(f)
    def wrapper(self, json_string, *args, **kwargs):
        return json.dumps(
          f(self, load_json(json_string), *args, **kwargs))
    return wrapper

# Prefer httplib2, with fallback to (std) httplib.
try:
    from httplib2 import Http

    def http_request(url, method, data=None, headers=None):
        h = Http()
        h.follow_redirects = False
	# headers = { "Content-type": "text/xml" }
	# headers = { "Content-type": "application/json" }
        return h.request(url, method, data, headers)

except ImportError:
    from urlparse import urlsplit
    from httplib import HTTPConnection, HTTPSConnection

    def http_request(url, method, data=None, headers=None):
        scheme, netloc, path, query, fragment = urlsplit(url)
        if scheme == 'https':
            conn = HTTPSConnection(netloc)
        else:
            conn = HTTPConnection(netloc)
	# headers = { "Content-type": "text/xml" }
	# headers = { "Content-type": "application/json" }
        conn.request(method, "%s?%s" % (path, query), data, headers=headers)
        response = conn.getresponse()
        response.get = response.getheader # NOTE: monkey-patch
        return response, response.read()


# Assertion utilities.

def expect(key, expected, value):
    assert expected == value, (
            "Expected %s to be %r but was %r" % (key, expected, value))

def expect_regexp(key, regexp, value):
    assert re.match(regexp, value), (
            "Expected %s to match regexp %r but was %r" % (key, regexp, value))

def expect_exists(what, value):
    assert value, "Expected %s to be present (got %r)." % (what, value)

def expect_not_exists(what, value):
    assert not value, "Expected no value for %s (got %r)." % (what, value)

# multipart post.

def post_multipart(host, selector, fields, files):
    """
    Post fields and files to an http host as multipart/form-data.
    fields is a sequence of (name, value) elements for regular form fields.
    files is a sequence of (name, filename, value) elements for data to be uploaded as files
    Return the server's response page.
    """
    print "test2"
    content_type, body = encode_multipart_formdata(fields, files)
    h = httplib.HTTP(host)
    
    h.putrequest('POST', selector)
    h.putheader('content-type', content_type)
    h.putheader('content-length', str(len(body)))
    h.endheaders()
    h.send(body)
    errcode, errmsg, headers = h.getreply()
    return headers, h.file.read()

def encode_multipart_formdata(fields, files):
    """
    fields is a sequence of (name, value) elements for regular form fields.
    files is a sequence of (name, filename, value) elements for data to be uploaded as files
    Return (content_type, body) ready for httplib.HTTP instance
    """
    BOUNDARY = '----------ThIs_Is_tHe_bouNdaRY_$'
    CRLF = '\r\n'
    L = []
    for (key, value) in fields:
        L.append('--' + BOUNDARY)
        L.append('Content-Disposition: form-data; name="%s"' % key)
        L.append('')
        L.append(value)
    for (key, filename, value) in files:
        L.append('--' + BOUNDARY)
        L.append('Content-Disposition: form-data; name="%s"; filename="%s"' % (key, filename))
        L.append('Content-Type: %s' % get_content_type(filename))
        L.append('')
        L.append(value)
    L.append('--' + BOUNDARY + '--')
    L.append('')
    body = CRLF.join(L)
    content_type = 'multipart/form-data; boundary=%s' % BOUNDARY
    return content_type, body

def get_content_type(filename):
    return mimetypes.guess_type(filename)[0] or 'application/octet-stream'

# The library core.


class CoreRestClient(object):

    def __init__(self):
        self._status = None
        self._reason = None
        self._baseurl = None
        self._send_headers = {}
        self._reset()

    def _reset(self):
        self._current_url = None
        self._response = None
        self._content = None

    def _do_request(self, method, url, data=None, format="xml"):
        self._reset()
        self._current_url = url = urljoin(self._baseurl, url)
	if format == "xml" :
		self._send_headers = { "Content-type": "application/xml" }

	if format == "json" :
		self._send_headers = { "Content-type": "application/json" }
	
	if format == "upload" : 
		self._send_headers = { "Content-type": "multipart/form-data; boundary= AaB03x" }

	if format == "urlencoded" :
		self._send_headers = { "Content-type": "application/x-www-form-urlencoded" }
	
	msg = "Format is: %s" % (format)
	url = url.replace(" ","+")
	print msg

	
        response, content = http_request(
                url, method, data, headers=self._send_headers)
        self._status, self._reason = response.status, response.reason
        self._response = response
        self._content = content
	
    def _do_file_request(self, url, method, field, file):
        print "test"
        response, content = post_multipart(
                url, method, field, file)
        self._response = response
        self._content = content

    def _get_header(self, header):
        return self._response.get(header.lower())

    def get_current_url(self):
        return self._current_url


class CoreRestLibrary(CoreRestClient):

    def base_url(self, url):
        self._baseurl = url

    def set_header(self, header, value):
        self._send_headers[header] = value

    def get(self, url):
	print "Request URL(GET):"
	print url
        self._do_request("GET", url)

    def head(self, url):
        self._do_request("HEAD", url)

    def post(self, url, data, format="xml"):
	print "Request URL(POST):"
	print url
        self._do_request("POST", url, data, format)

    def post_file(self, url, field, file):
	print "Request URL(POST FILE):"
	print url
        self._do_file_request(url, "POST", field, file)

    def put(self, url, data, format="xml"):
	print "Request URL(PUT):"
	print url
        self._do_request("PUT", url, data, format)

    def delete(self, url):
	print "Request URL(DELETE):"
	print url
        self._do_request("DELETE", url)

    def options(self, url):
        self._do_request("OPTIONS", url)

    def trace(self, url):
        self._do_request("TRACE", url)

    def patch(self, url, data):
        self._do_request("PATCH", url, data)

    def response_status(self, expected_status):
        if not expected_status.isdigit():
            status = httplib.responses[int(self._status)]
        else:
            status = "%d" % self._status
        expect("status", expected_status, status)

    def response(self, expected_response):
        response = "%d %s" % (self._status, self._reason)
        expect("status", expected_response, response)

    def follow(self):
        header = 'Location'
        c_loc = self._get_header(header)
        # FIXME: should only use Location (and check for 30x), right?
        if not c_loc:
            header = 'Content-Location'
            c_loc = self._get_header(header)
        expect_exists("response header '%s'" % header, c_loc)
        self.get(c_loc)

    def header(self, header, expected=None):
        value = self._get_header(header)
        expect(header, expected, value)

    def body_is(self, expected):
        expect("response body", expected, self._content)
    
    def body_has(self, expected):
	print self._content
	expect_regexp("response body", expected, self._content)
   
    def get_response(self):
	"""
        Fails if there is no response.
        Example:
        | get_response |
        """
	return self._content    

    def has_body(self):
        expect_exists("response body", self._content)
        return self._content

    def no_body(self):
        expect_not_exists("response body", self._content)

    def body_checksum_is(self, algorithm, expected):
        checksum = hashlib.new(algorithm, self._content).hexdigest()
        expect("body checksum", expected, checksum)
	
# json

    def should_be_valid_json(self, json_string):
        """
        Attempts to parse `json_string` as JSON. Fails, if `json_string` is invalid JSON.

        Example:
        | Should Be Valid Json | {"foo": "bar"} |
        """
        self.parse_json(json_string)

    def parse_json(self, json_string):
        """
        Parses the JSON document `json_string` and returns a Python datastructure.

        Example:
        | ${result}=       | Parse Json  | [1, 2, 3] |
        | Length Should Be | ${result}   | 3         |
        """
        return load_json(json_string)

    @_with_json
    def get_json_value(self, json_string, json_pointer):
        """
        Get the target node of the JSON document `json_string` specified by `json_pointer`.

        Example:
        | ${result}=       | Get Json Value   | {"foo": {"bar": [1,2,3]}} | /foo/bar |
        | Should Be Equal  | ${result}        | [1, 2, 3]                 |          |
        """
        return jsonpointer.resolve_pointer(json_string, json_pointer)

    def json_value_should_equal(self, json_string, json_pointer, expected_value):
        """
        Fails if the value of the target node of the JSON document
        `json_string` specified by JSON Pointer `json_pointer` is not `expected_value`.

        Example:
        | Set Test Variable       | ${doc}  | {"foo": {"bar": [1,2,3]}} |             |
        | Json Value Should Equal | ${doc}  | /foo/bar                  | "[1, 2, 3]" |
        """

        got = self.get_json_value(json_string, json_pointer)

        assert got == expected_value, \
               'JSON value "%s" does not equal "%s", but should have.' % (got, expected_value)


    def json_value_should_not_equal(self, json_string, json_pointer, expected_value):
        """
        Fails if the value of the target node of the JSON document
        `json_string` specified by JSON Pointer `json_pointer` is `expected_value`.

        Example:
        | Set Test Variable           | ${doc}  | {"foo": {"bar": [1,2,3]}} |             |
        | Json Value Should Not Equal | ${doc}  | /foo/bar                  | "[1, 2, 3]" |
        """

        got = self.get_json_value(json_string, json_pointer)

        message = 'JSON value "%s" does not equal "%s"' % (got, expected_value)

        assert got != expected_value, "%s, but should have." % message

        logger.debug("%s." % message)


    @_with_json
    def set_json_value(self, json_string, json_pointer, json_value):
        """
        Set the target node of the JSON document `json_string` specified by
        JSON Pointer `json_pointer` to `json_value`.

        Example:
        | ${result}=       | Set Json Value | {"foo": {"bar": [1,2,3]}} | /foo | 12 |
        | Should Be Equal  | ${result}      | {"foo": 12}               |      |    |
        """
        value = load_json(json_value)
        p = jsonpointer.set_pointer(json_string, json_pointer, value)
        return json_string

    @_with_json
    def log_json(self, json_string, log_level='INFO'):
        """
        Logs a pretty printed version of the JSON document `json_string`.
        """
        for line in json.dumps(json_string, indent=2).split('\n'):
            logger.write(line, log_level)

    # debug

    def show_response_body_in_browser(self):
        """
        Opens your default web browser with the last request's response body.

        This is meant for debugging response body's with complex media types.
        """
        self.context.response.showbrowser()




# Final library class (with ad hoc mixin support).

class RestLibrary(CoreRestLibrary):

    @classmethod
    def mixin(cls, klass):
        cls.__bases__ += (klass,)

    def __init__(self):
        self.__base_call('__init__')

    def _reset(self):
        self.__base_call('_reset')

    def __base_call(self, name, *args, **kwargs):
        for base in type(self).__bases__:
            if hasattr(base, name):
                getattr(base, name)(self, *args, **kwargs)


# Additional feature mixins.


class XmlMixinSupport(object):

    def __init__(self):
        self._namespaces = {}

    def _reset(self):
        self._lazy_xml = None

    def xmlns(self, pfx, uri):
        self._namespaces[pfx] = uri

    def xpath_value(self, expr, expected):
        value = self.find_xpath(expr)
        #expect("xpath %r" % expr, expected, value)
        return value

    def xpath_regexp(self, expr, regexp):
        value = self.find_xpath(expr)
        expect_regexp("xpath %r" % expr, regexp, value)
        return value

    def find_xpath_nodes(self, expr):
        return self.find_xpath(expr, False)

    def find_xpath(self, expr, tostr=True):
        value = self._eval_xpath(expr)
        expect_exists("xpath %r" % expr, value)
        if tostr:
            value = "".join(str(v) for v in value)
        return value

    def no_xpath(self, expr):
        value = self._eval_xpath(expr)
        expect_not_exists("xpath %r" % expr, value)

    def _get_parsed_xml(self):
        raise NotImplementedError("No XML parser available.")

    def _eval_xpath(self, expr):
        raise NotImplementedError("No XPath processor available.")




try:
    from lxml import etree

    class XmlMixin(XmlMixinSupport):

        def _get_parsed_xml(self):
            if self._lazy_xml is None:
                self._lazy_xml = etree.fromstring(self._content)
            return self._lazy_xml
	def get_parsed_xml(self):
            if self._lazy_xml is None:
                self._lazy_xml = etree.fromstring(self._content)
            return self._lazy_xml

        def _eval_xpath(self, expr):
            doc = self._get_parsed_xml()
	   
            return doc.xpath(expr, namespaces=self._namespaces)
	
	
	def should_be_valid_xml(self, xml_string):
	    return "To be added"

	def get_xml_value(self, xml_string):
	    count=0
            doc = self._get_parsed_xml()
	    r = doc.xpath(xml_string)
			
	  
	    for xml_text in r:
		#print xml_text.tag
		#if xml_text.tag == xml_string:
		#print xml_text.text
		count = count+1

	    if count>1:
		text="More than 1"
	    elif count==0:
		text="Null"
	    else:
		text=xml_text.text

            return text

	def get_xml_last_value(self, xml_string):
            doc = self._get_parsed_xml()
	    r = doc.xpath(xml_string)	
	  
	    for xml_text in r:
		#print xml_text.tag
		#print xml_text.text
		text=xml_text.text


            return text
	
	def get_xml_first_value(self, xml_string):
	    count=0
	    if xml_string!="":
		    doc = self._get_parsed_xml()
		    r = doc.xpath(xml_string)	
		    for xml_text in r:
			#print xml_text.tag
			#print xml_text.text
			count = count+1
			if count == 1:
				text=xml_text.text

	    if count == 0:
		text="Null"

            return text
                   
	def check_xml_value(self, xml_string, check_string):
	    """
	    Check for a given value in parsed xml and returns true on value found or else false
	    
	    Example:
	    |check_xml_value | xpath | searchstring
	    
	    """
	    count=0
	    result="false"
	    if xml_string!="":
		    doc = self._get_parsed_xml()
		    r = doc.xpath(xml_string)	
		    for xml_text in r:
			#print xml_text.tag
			#print xml_text.text
			count = count+1
			#if count == 1:
			#	text=xml_text.text
			text=xml_text.text
			#print text
			if text == check_string:
			   result="true"

            return result
            
	def get_xml_specific_value(self, xml_string, seq):
	    count=0
            doc = self._get_parsed_xml()
	    r = doc.xpath(xml_string)	
	  
	    for xml_text in r:
		#print xml_text.tag
		#print xml_text.text
		count = count+1
		if count == seq:
			text=xml_text.text

            return text
  

    RestLibrary.mixin(XmlMixin)

except ImportError:
    warn("Cannot parse XML responses. Missing module: lxml")
    # TODO: Try some more options (4Suite, elementtree, javax.xml).
    # If none can be found, disable XML keywords by removing the following line:
    RestLibrary.mixin(XmlMixinSupport)


# TODO: No features yet.
class JsonMixin(object):
    def _reset(self):
        self._lazy_json = None
RestLibrary.mixin(JsonMixin)


